package com.atguigu.gulimall.seckill.service.impl;

import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.execption.GmallException;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.DateUtil;
import com.atguigu.gmall.common.util.Jsons;
import com.atguigu.gmall.common.util.MD5;
import com.atguigu.gmall.common.utils.UserAuthUtils;
import com.atguigu.gulimall.order.entity.OrderInfo;
import com.atguigu.gulimall.seckill.entity.SeckillGoods;
import com.atguigu.gulimall.seckill.service.SeckillBizService;
import com.atguigu.gulimall.seckill.service.SeckillGoodsService;
import com.atguigu.gulimall.seckill.to.SeckillQueueMsg;
import com.atguigu.gulimall.util.consts.MqConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author nicc
 * @version 1.0
 * @className SeckillBizServiceImpl
 * @date 2022-10-28 11:16
 */

@Slf4j
@Service
public class SeckillBizServiceImpl implements SeckillBizService {

    @Autowired
    SeckillGoodsService goodsService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RabbitTemplate rabbitTemplate;

    //缓存当天需要参与秒杀的所有商品 (域缓存)
    //seckill:goods:2022-10-29
    Map<Long,SeckillGoods> localCache = new ConcurrentHashMap<>();

    /**
     * 从本地缓存优先获取所有商品，如果没有就同步远程redis的数据到本地缓存并返回
     * @return
     */
    //@MallCache
    @Override
    public List<SeckillGoods> getCurrentDaySeckillGoods() {

        //判断一级缓存中是否有数据
        if(localCache.values().size() > 0){
            //本地有数据，直接使用本地数据
            log.info("一级缓存命中....");
            List<SeckillGoods> collect = localCache.values()
                    .stream()
                    .sorted((Comparator.comparing(SeckillGoods::getStartTime)))
                    .collect(Collectors.toList());
            return collect;
        }

        //如果一级缓存没有命中，去redis中查
        log.info("一级缓存失效.正在同步远程数据...");

        //格式化的当天时间
        String formatDate = DateUtil.formatDate(new Date());

        //直接从缓存中查找
        List<Object> values = redisTemplate.opsForHash().values(RedisConst.SECKILL_GOOD_KEY + formatDate);

        //一一映射成原对象
        List<SeckillGoods> collect = values.stream()
                .map(item -> {
                    SeckillGoods seckillGoods = Jsons.toObj(item.toString(), SeckillGoods.class);
                    //同步到本地内存
                    localCache.put(seckillGoods.getSkuId(),seckillGoods);
                    return seckillGoods;
                })
                .sorted((Comparator.comparing(SeckillGoods::getStartTime)))
                .collect(Collectors.toList());

        return collect;
    }

    @Override
    public SeckillGoods getGoodsDetail(Long skuId) {
        SeckillGoods goods = localCache.get(skuId);
        if(goods == null){
            log.info("一级缓存未命中，正在同步redis数据");
            getCurrentDaySeckillGoods(); //同步redis

            //再次查询
            SeckillGoods seckillGoods = localCache.get(skuId);
            return seckillGoods;
        }

        log.info("一级缓存命中...");
        return goods;
    }


    @Override
    public void saveSeckillGoodsToCache(List<SeckillGoods> goodsByDate,
                                        String formatDate) {

        //1、保存每个商品详细信息
        BoundHashOperations<String, String, String> hashOps =
                redisTemplate.boundHashOps(RedisConst.SECKILL_GOOD_KEY + formatDate);

        for (SeckillGoods seckillGoods : goodsByDate) {
            //2、redis-保存商品
            hashOps.put(seckillGoods.getSkuId().toString(), Jsons.toStr(seckillGoods));

            //3、redis-保存库存  seckill:good:stockcount:2022-10-25:49
            String stockKey = RedisConst.SECKILL_GOOD_STOCK + formatDate + ":"+seckillGoods.getSkuId().toString();
            redisTemplate.opsForValue().set(stockKey,seckillGoods.getStockCount() + "",2,TimeUnit.DAYS);

            //4、给本地缓存同步一份
            localCache.put(seckillGoods.getSkuId(),seckillGoods);
        }

        //设置过期时间为2天
        hashOps.expire(2, TimeUnit.DAYS);
    }

    @Override
    public String generateSeckillCode(Long skuId) {
        String formatDate = DateUtil.formatDate(new Date());

        //1. 得到当前登录的用户id
        Long userId = UserAuthUtils.getUserAuthInfo().getUserId();

        //2. 拿到需要秒杀的商品【具有一级缓存获取商品功能】
        SeckillGoods goodsDetail = getGoodsDetail(skuId);

        //生成秒杀码前置校验
        //判断是否开始秒杀
        Date date = new Date();
        Date startTime = goodsDetail.getStartTime();
        Date endTime = goodsDetail.getEndTime();
        if(startTime.after(date)){
            //秒杀未开始
            throw new GmallException(ResultCodeEnum.SECKILL_NO_START);
        }

        if(endTime.before(date)){
            //秒杀已结束
            throw new GmallException(ResultCodeEnum.SECKILL_END);
        }

        //TODO 数据库扣库存，redis也扣
        //判断库存: 只要redis说没有库存一定没有，redis说有则不一定(数据库的库存redis)
        if(goodsDetail.getStockCount() <= 0){
            //没有库存了
            throw new GmallException(ResultCodeEnum.SECKILL_FINISH);
        }

        //3.生成秒杀码
        String code = computeCode(formatDate,userId,skuId);

        //4.将秒杀码缓存到redis中, 设置过期时间为1天
        String key = RedisConst.SECKILL_GOOD_CODE + code;
        if(!redisTemplate.hasKey(key)){
            redisTemplate.opsForValue()
                    .set(key,"0",1,TimeUnit.DAYS);
        }

        return code;
    }

    /**
     * 计算秒杀码
     * @param formatDate
     * @param userId
     * @param skuId
     * @return
     */
    public String computeCode(String formatDate, Long userId, Long skuId) {
        String encrypt = MD5.encrypt(formatDate + "_" + userId + "_" + skuId);
        return encrypt;
    }


    /**
     * 百万请求可能到要进来
     * @param skuId
     * @param skuIdStr
     */
    @Override
    public void queue(Long skuId, String skuIdStr) {
        Long userId = UserAuthUtils.getUserAuthInfo().getUserId();

        //1.验证秒杀码是否正确
        //再次计算这个码
        String code = computeCode(DateUtil.formatDate(new Date()), userId, skuId);
        //1、验证秒杀码是否正确  3 49 2022-10-25  ; 横向越权风险
        //1）、是否这个用户这个商品这一天的秒杀码？
        if(!code.equals(skuIdStr)){
            throw new GmallException(ResultCodeEnum.SECKILL_ILLEGAL);
        }

        //2）、是否经过正常的流程生成了秒杀码，在redis中
        if (!redisTemplate.hasKey(RedisConst.SECKILL_GOOD_CODE + skuIdStr)) {
            throw new GmallException(ResultCodeEnum.SECKILL_ILLEGAL);
        }

        //2、验证商品的秒杀时间
        SeckillGoods detail = getGoodsDetail(skuId);
        Date date = new Date();
        if(!detail.getStartTime().before(date)){
            throw new GmallException(ResultCodeEnum.SECKILL_NO_START);
        }

        if (!detail.getEndTime().after(date)) {
            throw new GmallException(ResultCodeEnum.SECKILL_END);
        }

        //3、验证商品库存；  快速判断是否有库存  【内存状态位快速校验】
        // 【库存的内存状态位】类似布隆过滤器思想。内存说有不一定有，内存说没有一定没有
        if (detail.getStockCount() <= 0) {
            throw new GmallException(ResultCodeEnum.SECKILL_FINISH);
        }

        //同样的一个用户同一天同一个商品秒杀单只能创建一个
        Long increment = redisTemplate.opsForValue().increment(RedisConst.SECKILL_GOOD_CODE + skuIdStr);
        if(increment <= 1){ //秒杀排队请求如果没发过就给MQ发消息 幂等性控制
            log.info("新秒杀排队请求进入.正在准备为他创建秒杀订单");

            //本地缓存库存数扣除 【扣本地】
            detail.setStockCount(detail.getStockCount()-1);
            //扣redis  seckill:good:stockcount:2022-10-25:49
            String key = RedisConst.SECKILL_GOOD_STOCK + DateUtil.formatDate(new Date()) + ":"+skuId;
            //【redis远程扣除】
            Long decrement = redisTemplate.opsForValue().decrement(key);

            if(decrement < 0){
                //前置拦截, 内存中数量为负就不发送消息
                throw new GmallException(ResultCodeEnum.SECKILL_FINISH);
            }

            //4、真正的排队秒杀【保存秒杀订单、扣除数据库库存】 发送秒杀单排队消息
            rabbitTemplate.convertAndSend(MqConst.SECKILL_EVENT_EXCHANGE,
                    MqConst.RK_SECKILL_QUEUE,
                    Jsons.toStr(new SeckillQueueMsg(userId,skuId,skuIdStr)));
        }else {
            //如果发过请求，再发同样的请求进来就通知用户正在排队中
            // throw new GmallException(ResultCodeEnum.SECKILL_RUN);
        }

    }

    @Override
    public ResultCodeEnum checkOrder(Long skuId) {
        //得到用户Id
        Long userId = UserAuthUtils.getUserAuthInfo().getUserId();

        //1、如果秒杀请求能处理成功（第一次扣库存）。
        //      给redis中缓存一个秒杀单(orderInfo[默认都是null])
        //2、如果秒杀请求已经扣过库存下过单。确认了用户的收货信息
        //      给redis中缓存一个秒杀单(orderInfo[有收货人信息,数据库会有单，订单id会有])

        //去Redis查是否有秒杀单数据。如果有看到哪一步了。
        String code = computeCode(DateUtil.formatDate(new Date()), userId, skuId);
        String key = RedisConst.SECKILL_ORDER + code;

        //从redis中获取订单的json
        String json = redisTemplate.opsForValue().get(key);
        if(StringUtils.isEmpty(json)){
            //redis中没订单是正在排队
            return ResultCodeEnum.SECKILL_RUN;
        }else {
            //redis中有订单
            if(RedisConst.SECKILL_ORDER_NO_STOCK.equals(json)){
                return ResultCodeEnum.SECKILL_FINISH;
            }else {
                //订单有id；就是下单成功；
                OrderInfo orderInfo = Jsons.toObj(json, OrderInfo.class);
                if(orderInfo.getId() != null){
                    return ResultCodeEnum.SECKILL_ORDER_SUCCESS;
                }else {
                    //订单没有id；就是抢单成功
                    return ResultCodeEnum.SECKILL_SUCCESS;
                }

            }
        }
    }

    @Override
    public OrderInfo getSeckillOrder(Long skuId) {
        Long userId = UserAuthUtils.getUserAuthInfo().getUserId();
        String code = computeCode(DateUtil.formatDate(new Date()), userId, skuId);

        String json = redisTemplate.opsForValue().get(RedisConst.SECKILL_ORDER + code);
        if(!StringUtils.isEmpty(json)){
            return Jsons.toObj(json,OrderInfo.class);
        }
        return null;
    }

    @Override
    public void updateSeckillOrder(OrderInfo orderInfo) {
        Long skuId = orderInfo.getOrderDetailList().get(0).getSkuId();
        Long userId = UserAuthUtils.getUserAuthInfo().getUserId();
        String code = computeCode(DateUtil.formatDate(new Date()), userId, skuId);

        redisTemplate.opsForValue().set(RedisConst.SECKILL_ORDER + code,Jsons.toStr(orderInfo));
    }
}
